#pragma once

class Socket
{
public:
	enum SocketType
	{
		// Connection oriented socket
		Stream = SOCK_STREAM,
		// Not connection oriented socket
		DataGram = SOCK_DGRAM,
	};

	enum SocketFamily
	{
		// Internet protocol version 4
		IPv4 = AF_INET,
		// Internet protocol version 6
		IPv6 = AF_INET6,
	};

	enum SocketProtocol
	{
		// Connection oriented protocol
		Tcp = IPPROTO_TCP,
		// Not connection oriented protocol
		Udp = IPPROTO_UDP,
	};

	struct HostEntry
	{
		HostEntry() : isIPv6(false), IPv4(0xFFFFFFFF), port(-1) {
		}

		bool isIPv6;
		union 
		{
			struct {
				uint64 lowPart;
				uint64 highPart;
			} IPv6;

			uint32 IPv4;
		};
		int16 port;

		std::string toString() const {
			if(isIPv6 == false) {
				in_addr addr;
				addr.s_addr = IPv4;
				std::stringstream strm;
				strm << inet_ntoa(addr);
				if(port != -1)
					strm << ":" << port;
				return strm.str();
			}
			return "IPv6 not supported yet...";
		}
	};

protected:
	SOCKET mSocket;
	WSADATA mWsdData;
	bool mWsaCreated;
	SocketFamily mFamily;
	SocketType mType;
	SocketProtocol mProtocol;
	HostEntry mRemotePoint;


	Socket(const Socket&) { }
	Socket& operator = (const Socket&) { return *this; }
	Socket(SOCKET native, SocketType type = Stream, SocketFamily family = IPv4, SocketProtocol proto = Tcp);
public:
	// Creates a socket using the given properties. Default is a connection oriented stream socket with internet protocol version 4
	Socket(SocketType type = Stream, SocketFamily family = IPv4, SocketProtocol proto = Tcp);
	~Socket();

	// Resolves the numerical address of a given host or ip. The returned HostEntry is either IPv4 or IPv6 depending on the
	// type of socket you created (IPv4 or IPv6). If no suitable entry is found SocketException is thrown.
	HostEntry resolveHost(const std::string& hostOrIp);
	// Connects to a host or an ip with a port. On error SocketException is thrown
	void connect(const std::string& hostOrIp, uint16 port);
	// Disconnects the socket and destroys it.
	void disconnect();

	// Binds the socket to an IP end point using an address and a port.
	void bind(uint16 port, const std::string& hostOrIp = "0.0.0.0");
	// Enters the listening state.
	void listen(uint16 backlog = 10);
	// Accepts a new socket. Returned value must be freed using delete.
	Socket* acceptSocket();

	// Sends a chunk of data where pData points to numBytes of readable memory.
	void send(const void* pData, uint32 numBytes);

	// Sends the variable val as a chunk of data.
	template<typename T>
	void send(const T& val) {
		send(&val, sizeof(T));
	}

	template<>
	void send<std::vector<uint8>>(const std::vector<uint8>& val) {
		if(val.size() == 0)
			return;

		send(&val[0], val.size());
	}

	// Receives numBytes of data from the socket. pBuffer must point to numBytes of writable memory
	void receive(void* pBuffer, uint32 numBytes);

	// Receives enough bytes from the network to fill the variable buffer.
	template<typename T>
	void receive(T& buffer) {
		receive(&buffer, sizeof(T));
	}

	template<>
	void receive<std::vector<uint8>>(std::vector<uint8>& buffer) {
		receive(&buffer[0], buffer.size());
	}

	void receiveAll(std::vector<uint8>& buffer) {
		uint32 numBytes = getBytesAvailable();
		buffer.resize(numBytes);
		receive(buffer);
	}

	// Reads a variable of type T from the socket.
	template<typename T>
	T receive() {
		T ret;
		receive(ret);
		return ret;
	}

	// Enables or disables the blocking state of the socket.
	void setBlocking(bool blocking);
	// Returns the number of bytes that can be read with one call to recv at the moment without blocking.
	uint32 getBytesAvailable();
	// Enables or disables the nagle algorithm (http://en.wikipedia.org/wiki/Nagle's_algorithm)
	void setNagle(bool enabled);

	const HostEntry& getRemotePoint() const { return mRemotePoint; }
};